package org.ecype.diego;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import android.net.Uri;
import android.util.Log;
import androidx.persistence.ManyToOne;
import androidx.persistence.OneToMany;
import androidx.persistence.OneToOne;
import androidx.persistence.PersistenceException;

public class Entity implements Iterable<Column> {

	private final Contract contract;
	private final Class<?> entityClass;
	private final String name;
	private final Set<Column> columns;
	private final Set<Reference> referencesToParent;
	private final Set<Reference> referencesToChild;
	private Column id;
	private final Uri uri;
	private UpdateCallback updateCallback;

	public Entity(Contract contract, Class<?> entity) throws IllegalAccessException {
		this.contract = contract;
		this.entityClass = entity;
		String className = entity.getName();
		name = className.substring(className.lastIndexOf("$") + 1).toLowerCase();

		int modifiers = entity.getModifiers();
		if (!Modifier.isStatic(modifiers)) {
			throw new PersistenceException("@Entity " + className + " must be static class");
		}
		if (Modifier.isPrivate(modifiers)) {
			throw new PersistenceException("@Entity " + className + " must not be private class");
		}

		uri = Uri.parse("content://" + contract.getAuthority() + "/" + name);

		columns = new HashSet<Column>();
		for (Field colunmField : Annotations.extractAnnotatedFields(entity, androidx.persistence.Column.class)) {
			Column column = Column.build(colunmField);
			if (column.isId()) {
				if (id == null) {
					id = column;
				} else {
					throw new PersistenceException("@Id must be used once for Entity " + className);
				}
			}
			columns.add(column);
		}

		if (id == null) {
			extractId(entity, className);
		}

		referencesToParent = new HashSet<Reference>();
		referencesToChild = new HashSet<Reference>();
		// for (Field reference : Annotations.extractAnnotatedFields(entity,
		// OneToMany.class)) {
		// columns.add(Column1.buildOneToMany(reference));
		// }

		Class<? extends UpdateCallback> callbackClass = null;
		Update updateAnnotaion = entity.getAnnotation(Update.class);
		if (updateAnnotaion != null) {
			callbackClass = updateAnnotaion.callback();
		}
		if (callbackClass == null) {
			callbackClass = RecreateTableOnUpdate.class;
		}
		setupUpdateCallback(callbackClass);
	}

	private void setupUpdateCallback(Class<? extends UpdateCallback> callbackClass) {
		try {
			updateCallback = callbackClass.newInstance();
		} catch (Exception e) {
			Log.e("Entity", "error creating update callback for class " + callbackClass, e);
		}
	}

	public UpdateCallback getUpdateCallback() {
		return updateCallback;
	}
	
	private void extractId(Class<?> entity, String className) throws IllegalAccessException {
		List<Field> idColumns = Annotations.extractAnnotatedFields(entity, androidx.persistence.Id.class);
		switch (idColumns.size()) {
		case 0:
			throw new PersistenceException("@Id no found for " + className);

		case 1:
			id = Column.buildId(idColumns.get(0));
			columns.add(id);
			break;

		default:
			throw new PersistenceException("@Id must be used once for Entity " + className);
		}
	}

	void setupReferences() throws IllegalAccessException {

		for (Field field : Annotations.extractAnnotatedFields(entityClass, OneToMany.class)) {
			OneToMany annotation = field.getAnnotation(OneToMany.class);
			Entity child = contract.findByClass(annotation.targetEntity());

			Reference reference = child.findReferenceToParent(this);
			if (reference == null) {
				reference = new Reference(this, child, field);
				child.referencesToParent.add(reference);
			}
			reference.setLinkToChild((String) field.get(null));
			referencesToChild.add(reference);
		}

		for (Field field : Annotations.extractAnnotatedFields(entityClass, ManyToOne.class)) {
			ManyToOne annotation = field.getAnnotation(ManyToOne.class);
			Entity parent = contract.findByClass(annotation.targetEntity());

			Reference reference = parent.findReferenceToChild(this);
			if (reference == null) {
				reference = new Reference(parent, this, field);
				referencesToParent.add(reference);
			}
			reference.setup(annotation);
			Column column = Column.buildLink(reference, field);
			reference.setLinkToParent(column.getName());
			parent.referencesToChild.add(reference);
			columns.add(column);
		}

		for (Field field : Annotations.extractAnnotatedFields(entityClass, OneToOne.class)) {
			OneToOne annotation = field.getAnnotation(OneToOne.class);
			Entity parent = contract.findByClass(annotation.targetEntity());

			Reference reference = parent.findReferenceToChild(this);
			if (reference == null) {
				reference = new Reference(parent, this, field);
				referencesToParent.add(reference);
			}
			reference.setup(annotation);
			Column column = Column.buildLink(reference, field);
			reference.setLinkToParent(column.getName());
			parent.referencesToChild.add(reference);
			columns.add(column);
		}

	}

	private Reference findReferenceToChild(Entity child) {
		for (Reference reference : referencesToChild) {
			if (reference.getChild().equals(child)) {
				return reference;
			}
		}
		return null;
	}

	private Reference findReferenceToParent(Entity parent) {
		for (Reference reference : referencesToParent) {
			if (reference.getParent().equals(parent)) {
				return reference;
			}
		}
		return null;
	}

	public String getName() {
		return name;
	}

	public Set<Entity> getReferenceEntities() {
		Set<Entity> references = new HashSet<Entity>();
		for (Reference reference : this.referencesToParent) {
			references.add(reference.getParent());
		}
		return references;
	}

	public Set<Reference> getReferencesToParent() {
		return referencesToParent;
	}

	public Column getIdColumn() {
		return id;
	}

	public Class<?> getEntryClass() {
		return entityClass;
	}

	@Override
	public String toString() {
		return name;
	}

	@Override
	public Iterator<Column> iterator() {
		List<Column> columns = new ArrayList<Column>(this.columns);
		Collections.sort(columns);
		return columns.iterator();
	}

	public Uri getUri() {
		return uri;
	}

	public Set<Reference> getReferencesToChild() {
		return referencesToChild;
	}

}
